﻿namespace EasyWeb.Data.EntityFramework
{
    using System;
    using System.Collections.Generic;
    using System.Data.Entity;
    using System.Data.Entity.Core;
    using System.Data.Entity.Infrastructure;
    using System.Linq;
    using System.Threading.Tasks;
    using System.Transactions;
    using EasyWeb.Data.EntityFramework.Models;
    using EasyWeb.Security;

    public abstract class EWDao
    {
        #region Select helpers

        internal virtual async Task<IEnumerable<TDomain>> SelectCoreAsync<TDomain, TEntity>(
            Func<TEntity, TDomain> converter = null)
            where TEntity : class
        {
            if (converter == null)
            {
                converter = DefaultConverter<TDomain, TEntity>;
            }

            return await Execute(async db =>
            {
                var entities = await db.Set<TEntity>().ToArrayAsync();
                return entities.Select(entity =>
                    {
                        return converter(entity);
                    }).ToArray();
            });
        }

        internal virtual async Task<TDomain> SelectCoreAsync<TDomain, TEntity>(
            object id,
            Func<TEntity, TDomain> converter = null)
            where TEntity : class
        {
            if (converter == null)
            {
                converter = DefaultConverter<TDomain, TEntity>;
            }

            return await Execute(async db =>
            {
                var entity = await db.Set<TEntity>().FindAsync(id);
                return converter(entity);
            });
        }

        #endregion

        #region Create helpers

        internal virtual async Task CreateCoreAsync<TDomain, TEntity>(
            TDomain domain,
            Func<TDomain, TEntity> converter = null,
            Action<TEntity, Tuple<Guid, DateTime, string>> entityInitializer = null,
            Action<TDomain, Tuple<Guid, DateTime, string, byte[]>> domainInitializer = null)
            where TEntity : class
        {
            if (converter == null)
            {
                converter = DefaultConverter<TDomain, TEntity>;
            }
            if (entityInitializer == null)
            {
                entityInitializer = DefaultEntityInitializer<TEntity>;
            }
            if (domainInitializer == null)
            {
                domainInitializer = DefaultDomainInitializer<TDomain>;
            }

            dynamic entity = converter(domain);
            entityInitializer(entity, new Tuple<Guid, DateTime, string>(Guid.NewGuid(), DateTime.UtcNow, EWUser.Current.Name));

            await Execute(async db =>
            {
                db.Set<TEntity>().Add(entity);

                var result = await db.SaveChangesAsync();
                if (result == 0)
                {
                    throw new EWDataConcurrencyException();
                }

                await db.Entry(entity).ReloadAsync();

                return default(object);
            });

            domainInitializer(domain, new Tuple<Guid, DateTime, string, byte[]>(entity.Id, entity.Created, entity.CreatedBy, entity.Timestamp));
        }

        #endregion

        #region Update helpers

        internal virtual async Task UpdateCoreAsync<TDomain, TEntity>(
            TDomain domain,
            Func<TDomain, TEntity> converter = null,
            Action<TEntity, Tuple<DateTime, string>> entityInitializer = null,
            Action<TDomain, Tuple<DateTime, string, byte[]>> domainInitializer = null)
            where TEntity : class
        {
            if (converter == null)
            {
                converter = DefaultConverter<TDomain, TEntity>;
            }
            if (entityInitializer == null)
            {
                entityInitializer = DefaultEntityInitializer<TEntity>;
            }
            if (domainInitializer == null)
            {
                domainInitializer = DefaultDomainInitializer<TDomain>;
            }

            dynamic entity = converter(domain);
            entityInitializer(entity, new Tuple<DateTime, string>(DateTime.UtcNow, EWUser.Current.Name));

            await Execute(async db =>
            {
                var entry = db.Entry(entity);
                entry.State = EntityState.Modified;
                entry.Property("Created").IsModified = false;
                entry.Property("CreatedBy").IsModified = false;

                var result = await db.SaveChangesAsync();
                if (result == 0)
                {
                    throw new EWDataConcurrencyException();
                }

                await db.Entry(entity).ReloadAsync();

                return default(object);
            });

            domainInitializer(domain, new Tuple<DateTime, string, byte[]>(entity.Modified, entity.ModifiedBy, entity.Timestamp));
        }

        #endregion

        #region Delete helpers

        internal virtual async Task DeleteCoreAsync<TEntity>(
            Guid id,
            byte[] timestamp,
            Action<TEntity, Tuple<byte[]>> entityInitializer = null)
            where TEntity : class
        {
            if (entityInitializer == null)
            {
                entityInitializer = DefaultEntityInitializer<TEntity>;
            }

            await Execute(async db =>
            {
                var entity = db.Set<TEntity>().Find(id);
                if (entity == null)
                {
                    throw new EWDataConcurrencyException();
                }

                entityInitializer(entity, new Tuple<byte[]>(timestamp));

                db.Entry(entity).State = EntityState.Deleted;

                var result = await db.SaveChangesAsync();
                if (result == 0)
                {
                    throw new EWDataConcurrencyException();
                }

                return default(object);
            });
        }

        #endregion

        #region General helpers

        // used by select
        internal virtual TDomain DefaultConverter<TDomain, TEntity>(TEntity entity)
            where TEntity : class
        {
            return (TDomain)EWModelHelpers.ToDomain((dynamic)entity);
        }

        // used by create and update
        internal virtual TEntity DefaultConverter<TDomain, TEntity>(TDomain domain)
            where TEntity : class
        {
            return (TEntity)EWModelHelpers.ToEntity((dynamic)domain);
        }

        // used by create
        internal virtual void DefaultEntityInitializer<TEntity>(TEntity entity, Tuple<Guid, DateTime, string> parameters)
            where TEntity : class
        {
            dynamic proxy = entity;
            proxy.Id = parameters.Item1;
            proxy.Created = parameters.Item2;
            proxy.Modified = parameters.Item2;
            proxy.CreatedBy = parameters.Item3;
            proxy.ModifiedBy = parameters.Item3;
        }

        // used by create
        internal virtual void DefaultDomainInitializer<TDomain>(TDomain domain, Tuple<Guid, DateTime, string, byte[]> parameters)
        {
            dynamic proxy = domain;
            proxy.Id = parameters.Item1;
            proxy.Created = parameters.Item2;
            proxy.Modified = parameters.Item2;
            proxy.CreatedBy = parameters.Item3;
            proxy.ModifiedBy = parameters.Item3;
            proxy.Timestamp = parameters.Item4;
        }

        // used by update
        internal virtual void DefaultEntityInitializer<TEntity>(TEntity entity, Tuple<DateTime, string> parameters)
            where TEntity : class
        {
            dynamic proxy = entity;
            proxy.Modified = parameters.Item1;
            proxy.ModifiedBy = parameters.Item2;
        }

        // used by update
        internal virtual void DefaultDomainInitializer<TDomain>(TDomain domain, Tuple<DateTime, string, byte[]> parameters)
        {
            dynamic proxy = domain;
            proxy.Modified = parameters.Item1;
            proxy.ModifiedBy = parameters.Item2;
            proxy.Timestamp = parameters.Item3;
        }

        // used by delete
        internal virtual void DefaultEntityInitializer<TEntity>(TEntity entity, Tuple<byte[]> parameters)
            where TEntity : class
        {
            dynamic proxy = entity;
            proxy.Timestamp = parameters.Item1;
        }

        internal virtual async Task<TResult> Execute<TResult>(Func<EWDbContext, Task<TResult>> executor)
        {
            var db = default(EWDbContext);

            try
            {
                db = new EWDbContext();
                return await executor(db);
            }
            catch (DbUpdateConcurrencyException ex)
            {
                throw new EWDataConcurrencyException(ex.Message, ex);
            }
            catch (OptimisticConcurrencyException ex)
            {
                throw new EWDataConcurrencyException(ex.Message, ex);
            }
            catch (EWDataException)
            {
                throw;
            }
            catch (Exception ex)
            {
                throw new EWDataException("There was a database error occurred, please see inner exception for details.", ex);
            }
            finally
            {
                if (db != null) db.Dispose();
            }
        }

        #endregion
    }
}
